package com.bzyd.ss.config;

import com.bzyd.ss.encoder.MD5PasswordEncoder;
import com.bzyd.ss.encoder.SimplePasswordEncoder;
import com.bzyd.ss.handler.MyAccessDeniedHandler;
import com.bzyd.ss.handler.MyAuthenticationFailureHandler;
import com.bzyd.ss.handler.MyAuthenticationSuccessHandler;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

@Configuration
public class MySpringSecurityConfiguration extends WebSecurityConfigurerAdapter {

    /**
     * 配置密码加密器
     */
    @Bean
    public PasswordEncoder getPasswordEncoder(){
        // return new SimplePasswordEncoder();// 使用简单加密器
        // return new MD5PasswordEncoder();// 使用MD5加密器
        return new BCryptPasswordEncoder();// 使用Spring Security提供的强散列加密器
    }

    /**
     * 重写配置逻辑
     * 注意：当提供自定义配置，删除super.configure方法的时候，所有的默认流程全部清空
     *
     * PS：
     * http.formLogin().loginProcessingUrl(String path)
     *      用户登录请求地址，即处理用户登录逻辑的地址
     *      SpringSecurity提供的处理登录请求控制器，是path监听软编码的，可以通过此方法动态配置监听地址
     *      只要配置地址和页面的请求地址一致，即可完成登录
     *      登录成功，默认返回的页面地址是"/"，有前置请求则默认返回前置地址
     *          如：直接访问loginPage()，登录成功进入"/"
     *          如：访问/ok，未登录，进入loginPage()，登录成功后，重新返回/ok
     *      登录失败，默认返回的页面地址是http.formLogin().loginPage(String path) 方法参数?error
     *
     * http.formLogin().defaultSuccessUrl(String path, boolean alwayUse)
     *      设置登录成功后，响应重定向地址，Spring Security要求必须传递绝对路径，如果传递的是相对路径，
     *      则相对于当前服务器的根，开始寻址，如：http://localhost:port/
     *      参数alwaysUse，可以省略，默认为false，配置为false时，通常重定向失败 ^_^，true代表一定使用这个重定向地址
     *
     * 登录请求参数命名：
     *      默认规则是：用户必须是username，密码必须是password
     *      此逻辑是由UsernamePasswordAuthenticationFilter决定的
     *
     * @param http 基于HTTP协议的Security配置对象，包含所有的Spring Security相关配置逻辑
     * @throws Exception 当配置出错的时候抛出
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {

        // 解决 H2 控制台 Refused to display 'http://localhost:8080/' in a frame because it set 'X-Frame-Options' to 'deny'.
        http.headers().frameOptions().disable();

        // 认证配置
        http.formLogin()
                .usernameParameter("name") // 设置请求参数中，用户名参数名称，默认是username
                .passwordParameter("pswd") // 设置请求参数中，密码参数名称，默认是password
                .loginPage("/page/toLogin") // 登录页面地址，默认/login，请求方式是GET
                .loginProcessingUrl("/user/login") // 登录逻辑接口，默认是/login，请求方式是POST
//                .successForwardUrl("/page/toMain");// 登录成功后，请求转发地址，PS：请求转发url不会改变，Security请求转发使用POST请求
//                .defaultSuccessUrl("/page/toMain",true);// 登录成功后，响应重定向地址，PS：请求重定向url会改变，使用GET方式
                .successHandler(new MyAuthenticationSuccessHandler("/page/toMain",true))// 使用自定义登录成功处理器，实现登录成功后的请求处理，重定向或转发
//                .failureForwardUrl("/page/toFailure")// 登录失败后，请求转发的地址，Security请求转发使用POST请求，默认转发到loginPage() ?error
//                .failureUrl("/page/toFailure");// 登录失败，响应重定向地址，PS：记得放开权限校验拦截
                .failureHandler(new MyAuthenticationFailureHandler("/page/toFailure",true));// 使用自定义登录失败处理器

        // 授权配置，PS：Spring Security先匹配路径后，再授予访问权限
        // 配置权限校验，如：什么地址必须认证后才能访问，什么地址可以不认证就访问
        // 如：登录页面，不认证就可以访问，其他页面必须认证才能访问
        // 权限校验的配置是线性的，从开始的配置位置开始校验，成功立即返回，校验匹配失败，继续后续的校验逻辑
        // 如：请求 /page/toLogin，校验antMatches()成功，授予所有权限，返回，后续的anyRequests()不执行
        // 如：请求 /abc，校验antMatches()失败，执行后续的anyRequests()校验，要求必须认证后才能访问
        // 所有的校验失败，都返回到 loginPage() 登录页面

        // 常用的匹配请求路径的方法：antMatchers、regexMatchers、anyRequest，执行顺序按照定义顺序执行，任何方法匹配就记录返回，后续逻辑不执行
        // antMatchers - 程序员最常用，是基于 *、/、/** 等匹配符号定义的表达式，其中 * 代表0到多个字符，不包括斜杠，/** 代表0到多个字符，包括斜杠
        // regexMatchers - 程序员排斥，但是推荐使用，效率相对比较高，是基于正则表达式的
        // anyRequest - 相当于 /** 的antMatchers匹配，即任意请求地址

        // 授予权限的方法，都是基于路径地址匹配基础上调用的
        // 常用的权限方法：
        // 1、permitAll() - 免登录访问，任意用户任意状态都可以访问，描述通用资源，如：网站首页、静态资源等
        // 2、anonymous() - 匿名访问，登录后不能访问，如：登录页面、登录请求
        // 3、authenticated() - 登录后可以访问，描述保护资源，如：个人信息、订单查看、密码修改等
        // 4、fullyAuthenticated() - 完整登录后才可以访问，描述敏感资源，如：钱的操作
        // 5、denyAll() - 拒绝访问，任意用户任意状态都不能访问
        // 6、rememberMe() - 记住我，自动登录/n天免登陆，描述非敏感资源，如：除钱相关操作之外的内容
        // 以上所有权限方法，底层调用都是基于统一的方法 access()
        // access() - 可以实现任何权限授予逻辑，是基于权限表达式的权限授予方法。而表达式规则是由Spring Security定义的
        http.authorizeRequests()
                .antMatchers("/page/toLogin").anonymous() // 匿名访问
                .antMatchers("/page/toFailure").permitAll() // toLogin请求地址，可以随便访问
                .antMatchers("/h2-console/**").permitAll() // H2 控制台相关接口免登录访问
                .regexMatchers(".*[.]css").permitAll() // 所有css资源随便访问
                .antMatchers("/**/*.js").permitAll() // 所有js资源随便访问

                // 基于角色的权限管理
                // hasRole("xxx") 相当于 access("hasRole('ROLE_xxx')")
                // hasAnyRole("x","y","z") 相当于 access("hasAnyRole('ROLE_x','ROLE_y','ROLE_z')")
//                .antMatchers("/admin/read").hasRole("管理员")
//                .antMatchers("/guest/read").hasAnyRole("管理员","访客")
//                .antMatchers("/guest/write").hasRole("访客")

                // 基于资源的权限管理
                // hasAuthority("xxx") 相当于 access("hasAuthority('xxx')")
                // hasAnyAuthority("xxx","yyy") 相当于 access("hasAnyAuthority('xxx','yyy')")
//                .antMatchers("/admin/read").hasAuthority("admin:read")
//                .antMatchers("/admin/write").access("hasAuthority('admin:write')")
//                .antMatchers("/guest/read").access("hasAnyAuthority('guest:read','admin:read')")
//                .antMatchers("/guest/write").hasAnyAuthority("guest:write")

                // 自定义权限管理，原理：access("xxx") 方法接受的参数是true|false字符串形式
                // 因此在access方法的参数中，可以使用SpringEL表达式传递代码运行逻辑
                // @beanName 可以获取Spring容器中的对象
                // @beanName.methodName() 可以调用对象中的方法
                // 方法参数要传递的对象，可以在WebSecurityExpressionRoot中查看，是这个类型中的所有属性
//                .antMatchers("/**").access("@customAuthorityPermit.hasAuthority(request, authentication)")

                .anyRequest().authenticated(); // 任意的请求，都必须认证后才能访问,PS：配置在最后

        // 配置403无权限报错处理器
        http.exceptionHandling().accessDeniedHandler(new MyAccessDeniedHandler());

        // 关闭CSRF安全协议
        // 一般不推荐用Spring Security框架做跨站请求伪造防护
        // 最好自己维护表单token，即在返回表单时，同时生成token做隐藏字段，在提交时同时提交token
        // 后端在处理成功后删除token，下次提交校验不对就报错
        http.csrf().disable();
    }
}
